package org.benetech.collections;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.jdom.Attribute;
import org.jdom.Element;

/**
 * Tree of Elements with Ids. Modeled from original tree.
 * @author Reuben Firmin
 *
 * @param <T> The type of the primary content
 * @param <U> The key for the meta content
 * @param <V> The type of the meta content
 */
public final class IdTree<T extends Element, U, V> extends Tree<T, U, V> {

	public static final String ID = "id";
	/** Next id to assign for a given element name. */
	private Map<String, Integer> elementIdIndices;
	/** List of ids for a given element name. */
	private ListMap<String, String> elementIds;
	private boolean childIds;

	/**
	 * Construct an IdTree based on the existing tree's structure. Apply unique ids to each of the elements.
	 * @param model The tree to copy the structure from
	 */
	public IdTree(final Tree<T, ? , ? > model) {
		this (model, true);
    }

	/**
	 * Construct an IdTree based on the existing tree's content. Apply unique ids to each of the elements. Copy children
	 * only if requested.
	 * @param model The tree to copy the structure from
	 * @param copyChildren True to recursively copy from the model
	 */
	public IdTree(final Tree<T, ? , ? > model, final boolean copyChildren) {
		this(model, null, null, copyChildren);
	}

	/**
	 * Construct a new instance.
	 * @param model The model to copy from
	 * @param elementIdIndices Null to create. Allows single reference to be propagated to children
	 * @param elementIds Null to create. Allows single reference to be propagated to children
	 * @param copyChildren True to recursively copy from the model
	 */
	private IdTree(final Tree<T, ? , ? > model, final Map<String, Integer> elementIdIndices,
			final ListMap<String, String> elementIds, final boolean copyChildren)
	{
		super(model.getContent());
		if (elementIdIndices == null) {
			this.elementIdIndices = new HashMap<String, Integer>();
		} else {
			this.elementIdIndices = elementIdIndices;
		}

		if (elementIds == null) {
			this.elementIds = new ListMap<String, String>();
		} else {
			this.elementIds = elementIds;
		}

		final int modelSize = model.getTopOfTree().getSize();
		applyId(this, modelSize);
		if (copyChildren) {
			copyAndIdChildren(this, model);
		}
	}

	/**
	 * Copy the children of the model tree to the IdTree being built out. Recursively calls back to the constructor.
	 * @param parent The parent
	 * @param nodeToCopy The node to copy
	 */
	private void copyAndIdChildren(final IdTree<T, U, V> parent, final Tree<T, ? , ? > nodeToCopy) {
		for (Tree<T, ? , ? > child : nodeToCopy.getChildren()) {
			parent.copyAndIdHelper((new IdTree(child, elementIdIndices, elementIds, true)));
		}
	}

	/**
	 * Helper Function to Copy and Id Children.  Adds Child without Re-Iding Child
	 * @param child The child to add
	 */
	private void copyAndIdHelper(final IdTree<T, U, V> child) {
		super.addChild(child);
	}



	/**
	 * Ensure that there is a unique Id on the tree node. Generate one if necessary
	 * @param tree The tree node
	 * @param modelSize The size of the tree
	 */
	private void applyId(final IdTree<T, U, V> tree, final int modelSize) {
		final String elementName = tree.getContent().getName();
		final String existingId = tree.getId();
		// if the ID doesn't exist, or it isn't unique, generate a new one in the overall tree id-space
		if (existingId == null || !((IdTree<T, U, V>) getTopOfTree()).isUniqueId(elementName, existingId)) {
			tree.setId(((IdTree<T, U, V>) getTopOfTree()).newId(elementName, modelSize));
		}
		((IdTree<T, U, V>) getTopOfTree()).elementIds.put(elementName, tree.getId());
	}

	/**
	 * Generate a new id, e.g. h2_000019.
	 *
	 * @param name The string before the _
	 * @param numIds The number of ids that will be generated
	 * @return The string representing the id
	 */
	private String newId(final String name, final int numIds) {
		//	set up the next id to assign
		if (elementIdIndices.get(name) == null) {
			elementIdIndices.put(name, 0);
		} else {
			elementIdIndices.put(name, elementIdIndices.get(name) + 1);
		}
		final int idNum = elementIdIndices.get(name);
		final int numPlaces = String.valueOf(numIds).length() + 2;
		return name + "_" + StringUtils.leftPad(String.valueOf(idNum), numPlaces, '0');
	}

	/**
	 * Returns whether the id is unique.
	 * @param name The name of the element being id'd
	 * @param id The id of the element
	 * @return true if unique
	 */
	private boolean isUniqueId(final String name, final String id) {
		return elementIds.get(name) == null || !elementIds.get(name).contains(id);
	}

	/**
	 * Set the id attribute of this Element.
	 * @param id never null
	 */
	public void setId(final String id) {
		// clean out attributes of name "iD", "ID", "Id", "id"

		for (Attribute attribute : (List<Attribute>) getContent().getAttributes()) {
			if (attribute.getName().equalsIgnoreCase(ID)) {
				getContent().removeAttribute(attribute);
				break;
			}
		}

		getContent().getAttributes().add(new Attribute(ID, id));
	}

	/**
	 * Get the Id of the Element in this Tree.
	 * @return never null
	 */
	public String getId() {
		return getContent().getAttributeValue(ID);
	}

	/**
	 * Whether children of this element have ids.
	 * @return never null
	 */
	public boolean hasChildIds() {
		return childIds;
	}


	/**
	 * Set whether children of this element have ids.
	 * @param childIds true or false
	 */
	public void setChildIds(final boolean childIds) {
		this.childIds = childIds;
	}

	/**
	 * Does the tree have an id attribute?
	 * @return true if there's an attribute
	 */
	public boolean hasId() {
		return getContent().getAttribute(ID) != null;
	}

	/**
	 * Intercepts the addChild call, and makes sure the child is given a unique id.
	 * @param child The idtree child to add
	 */
	public void addChild(final IdTree<T, U, V> child) {
		super.addChild(child);
		// overwrite the existing id, as it's wrong (generated within the child's id space)
		applyId(child, this.getTopOfTree().getSize());
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean equals(final Object other) {
		return ((other instanceof IdTree)
				&& ((IdTree) other).getContent().equals(getContent())
				&& ((IdTree) other).getId().equals(getId()));
	}

	/**
	 * {@inheritDoc}
	 */
	public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode());
        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
        return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public T getContent() {
		// this method is required so that getContent() returns an Element
		return super.getContent();
	}
}
